package com.xucia.jsponic.remote;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.ScriptRuntime;

import com.xucia.jsponic.JsponicRuntime;
import com.xucia.jsponic.data.GlobalData;
import com.xucia.jsponic.data.Identification;
import com.xucia.jsponic.data.ObjectId;
import com.xucia.jsponic.data.Persistable;
import com.xucia.jsponic.data.PersistableImpl;
import com.xucia.jsponic.data.PersistableList;
import com.xucia.jsponic.rpc.RPCall;
import com.xucia.jsponic.security.JavaScriptCodingPermission;
import com.xucia.jsponic.security.UserSecurity;

public abstract class JsponReceiver {
	static Pattern DATE_MATCHER = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$");
	static SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ");
	protected String path = "";
	/**
	 * Converts a string jspon representation of a value or object into an object 
	 * @param jsponString
	 * @return
	 */
	public Object convertJsponStringToObject(String jsponString) {
		try {
			Object value = new JSONTokener(jsponString).nextValue();
			if (value instanceof JSONArray)
				return listFromJSPONArray((JSONArray) value);
			if (value instanceof JSONObject)
				return convertIdIfNeeded(idFromJSPONObject((JSONObject) value));
			if (value instanceof String && DATE_MATCHER.matcher((String)value).matches())
				try {
					value = ClientConnection.ISO_SDF.parse(((String) value));
				} catch (ParseException e) {
					System.err.println("date error: " + e.getMessage());					
				}
			if (value == JSONObject.NULL)
				return null;
			return value;
		}
		catch (JSONException e) {
			e.printStackTrace();
			return jsponString;
		}
	}
	protected List listFromJSPONArray(JSONArray array) throws JSONException {
		List list = JsponicRuntime.newArray();
    	for (int i =0; i < array.length(); i++) {
    		Object value = array.get(i);
    		if (value instanceof JSONObject) {
    			value = convertIdIfNeeded(idFromJSPONObject((JSONObject) value));
    		}
    		else if (value instanceof JSONArray) {
    			value = listFromJSPONArray((JSONArray)value);		        			
    		}
    		list.add(value);
    	}
    	return list;
	} 
	public static final String FUNCTION_CODE_KEY = "function";
	static String functionCompression(String code) {
		//code = code.replaceAll("\\([\\w\\s,]*\\)\\{var njf(\\d+)=_frm\\(this,arguments([,\"\\w+\"]*)\\);nj:while\\(1\\)\\{switch\\(njf(\\d+).cp\\)\\{case 0:", "\u00ea$1$2\u00eb");
		//code = code.replaceAll("njf(\\d+).cp=(\\d+);case (\\d+):if\\(\\(njf(\\d+).rv(\\d+)=", "\u00ea$1,$2\u00ec");
		//code = code.replaceAll("\\)==NJSUS\\)\\{return njf(\\d+)\\(\\);\\}", "\u00ea$1\u00ed");

		code = code.replaceAll("\\([\\w|\\s|,]*\\)\\{with\\(_frm\\(this,arguments,([,\\[\\]\\\"\\w+\"]*)\\)\\)\\{nj:while\\(1\\)\\{switch\\(\\_cp\\)\\{case 0:", "\u00ea$1\u00eb");
		code = code.replaceAll("\\_cp=(\\d+);case (\\d+):if\\(\\(\\_r.v(\\d+)=", "\u00ea$1\u00ec");
		code = code.replaceAll("\\)==NJSUS\\)\\{return \\_S\\(\\);\\}", "\u00ea\u00ed");
		code = code.replaceAll("return;case -1:return _s()\\}\\}\\}\\}", "\u00ea\u00ee");
		//code = code.replaceAll("njf(\\d+)._", "\\xEC\\1");
		return code;
	}
	static interface ListUpdater{
		void update(Object object) throws JSONException;
	}
	static void doListUpdate(JSONObject updateObject, String command, ListUpdater updater)  throws JSONException {
		if (updateObject.has(command)) {
			JSONArray array = updateObject.getJSONArray(command);
	    	for (int i =0; i < array.length(); i++) {
	    		updater.update(array.get(i));
	    	}
		}
		
	}
	protected void updateList(final Persistable listToUpdate,JSONObject updateObject) throws JSONException  {
	   		doListUpdate(updateObject,"append", new ListUpdater() {
	   			public void update(Object object) throws JSONException{
	   				Object obj = convertIdIfNeeded(idOrValueFromJSON(object));
	   				if (listToUpdate instanceof List)
	   					((List) listToUpdate).add(obj);
	   				else {
		   				Context cx = Context.enter();
		   		        int length = ScriptRuntime.toInt32(ScriptRuntime.getObjectProp(listToUpdate, "length", cx));
		   		        ScriptRuntime.setObjectIndex(listToUpdate, length, obj, cx);
		   		        ScriptRuntime.setObjectProp(listToUpdate, "length", ScriptRuntime.wrapNumber(length+1), cx);
	   				}
	   			}
	   		});
	   		doListUpdate(updateObject,"delete", new ListUpdater() {
	   			public void update(Object object) throws JSONException{
	   				Object obj = convertIdIfNeeded(idOrValueFromJSON(object));
	   				if (listToUpdate instanceof List)
	   					((List) listToUpdate).remove(obj);
	   				else {
	   					int length = ScriptRuntime.toInt32(ScriptRuntime.getObjectProp(listToUpdate, "length", Context.enter()));
	   					for (int i = 0; i < length; i++)
	   						if (listToUpdate.get(i,listToUpdate) == obj) {
	   							NativeArray.splice(listToUpdate,new Object[]{i,1});
	   							break;
	   						}
	   				}
	   			}
	   		});
	   		doListUpdate(updateObject,"replace", new ListUpdater() {
	   			public void update(Object object) throws JSONException{
	   				Object old = idOrValueFromJSON(((JSONObject)object).get("old"));
	   				Object newObj = idOrValueFromJSON(((JSONObject)object).get("new"));
	   				if (listToUpdate instanceof List) {
	   					int index = ((List)listToUpdate).indexOf(old);
	   					((List)listToUpdate).set(index,newObj);
	   				}
	   				else {
	   					int length = ScriptRuntime.toInt32(ScriptRuntime.getObjectProp(listToUpdate, "length", Context.enter()));
	   					for (int i = 0; i < length; i++)
	   						if (listToUpdate.get(i,listToUpdate) == old) 
	   							NativeArray.splice(listToUpdate,new Object[]{i,1,newObj});
	   						
	   				}
	   					
	   			}
	   		});
	   		doListUpdate(updateObject,"splice", new ListUpdater() {
	   			public void update(Object obj) throws JSONException{
	   				JSONObject object = (JSONObject) obj;
	   				JSONArray values = null;
	   				if (object.has("values")) 
	   					values = object.getJSONArray("values");
	   				Object args[] = new Object[values == null ? 2 : values.length() + 2];
	   				args[0] = object.getInt("index");
	   				args[1] = object.getInt("howMany");
	   				if (values != null)
   			    	for (int i =0; i < values.length(); i++) 
   			    		args[i+2]=convertIdIfNeeded(idOrValueFromJSON(values.get(i)));
	   				NativeArray.splice(listToUpdate,args);
	   			}
	   		});
	}
	protected Persistable createInitialObject(JSONObject object) throws JSONException {
		return object.has("array") ? JsponicRuntime.newArray() : 
			object.has(GlobalData.FUNCTION_METHOD_FIELD) ?
					PersistableImpl.createFunction(object.getString(GlobalData.FUNCTION_METHOD_FIELD),
							"new function",("server".equals(object.opt("runAt")) || "both".equals(object.opt("runAt"))) && UserSecurity.userHasPermission(JavaScriptCodingPermission.instance))
					: JsponicRuntime.newObject((ObjectId) idOrValueFromJSON(object.remove(GlobalData.BASIS_FIELD)));
	}
	protected void replaceList(final PersistableList listToUpdate, List changes) {
    	int i = 0;
    	boolean rebuild = false;
		for (Object change : changes) {			
			if (listToUpdate.size() > i) { // make sure we are still matching
				if	(!listToUpdate.get(i).equals(change)) {
					rebuild = true;
					break;
				}
			}
			else
				listToUpdate.add(change);
			i++;
		}
		if (listToUpdate.size() != i)
			rebuild = true;
		System.err.println("rebuild: " + rebuild);
		if (rebuild) { 
			listToUpdate.clear();
			for (Object change : changes) 
				listToUpdate.add(change);
		}
	}
	public static class UpdateInfo extends NativeObject {
		JSONObject updateObject;
		public UpdateInfo() {
		}
		public UpdateInfo(JSONObject updateObject) {
			this.updateObject = updateObject;
		}
	}
	static protected Object NOT_READY_FIELD = new Object();
	protected Object idOrValueFromJSON(Object value) throws JSONException {
		if (value instanceof JSONObject) {
			if (((JSONObject) value).has("ready") && !((JSONObject) value).has("id") && ((JSONObject) value).getInt("ready") == 0) {
				return NOT_READY_FIELD;
			}
			return idFromJSPONObject((JSONObject) value);
		}
		else if (value instanceof JSONArray) {
			return listFromJSPONArray((JSONArray)value);
		}
		else if (value instanceof String) {
			if (((String) value).startsWith("@") && ((String) value).endsWith("@"))
				return new Date(Long.parseLong(((String)value).substring(1,((String)value).length()-1))); // handle dates
			if (DATE_MATCHER.matcher((String)value).matches())
				try {
					return SDF.parse(((String) value).replace('T', ' ').replace('Z', ' ') + "-0000");
				} catch (ParseException e) {
					System.err.println("date error: " + e.getMessage());					
				}
		}
		else if (value == JSONObject.NULL)
			return null;
		return value;
	}
	static Object convertIdIfNeeded(Object value) {
		if (value instanceof Identification)
			return ((Identification<? extends Object>)value).getTarget();
		return value;
	}
	public void handleRPC(Object targetObject,  JSONObject rpcObject) throws JSONException {
		Persistable target = (targetObject instanceof ObjectId) ? ((ObjectId)targetObject).getTarget() : (Persistable)targetObject;
		JSONArray params = (JSONArray) rpcObject.opt("params");
		Object[] paramValues;
		if (params == null) {
			paramValues = new Object[0];
		}
		else {
			paramValues = new Object[params.length()];
        	for (int j =0; j < params.length(); j++) {
        		paramValues[j] = convertIdIfNeeded(idOrValueFromJSON(params.get(j)));
        	}
		}			        				
		String method = rpcObject.getString("method");
		RPCall rpCall = new RPCall(target,method,paramValues,rpcObject.getString("id")); 
		rpCall.executeLocally(); // responses will be added the current connection

	}
	public static ObjectId getErasureEntity() {
		if (ERASURE_ENTITY == null)
			ERASURE_ENTITY = new ObjectId() {};
		return ERASURE_ENTITY;
	}
	static ObjectId ERASURE_ENTITY;
	protected abstract Identification<? extends Object> idFromJSPONObject(JSONObject object) throws JSONException;
	public String getPath() {
		return path;
	}
	public void setPath(String path) {
		this.path = path;
	}
	
	
	
}
